package com.brutal.utils;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import com.rsbuddy.script.methods.Game;
import com.rsbuddy.script.methods.Players;
import com.rsbuddy.script.methods.Walking;
import com.rsbuddy.script.wrappers.Path;
import com.rsbuddy.script.wrappers.Tile;
import com.rsbuddy.script.wrappers.TilePath;

public class PathFinder extends Path {

	private TilePath tilePath = null;
	private final Tile start;
	private final Tile end;
	private int[][] flags = null;
	private Tile[] tiles = null;

	private final static int BLOCK_0 = 0x100;
	private final static int BLOCK_1 = 0x40000;
	private final static int BLOCK_2 = 0x200000;
	private final static int BLOCK_3 = 0x40000000;
	private final static int BLOCK_0_NORTH = 0x2;
	private final static int BLOCK_0_EAST = 0x8;
	private final static int BLOCK_0_SOUTH = 0x20;
	private final static int BLOCK_0_WEST = 0x80;
	private final static int BLOCK_1_NORTH = 0x400;
	private final static int BLOCK_1_EAST = 0x1000;
	private final static int BLOCK_1_SOUTH = 0x4000;
	private final static int BLOCK_1_WEST = 0x10000;
	private final static int BLOCK_2_NORTH = 0x800000;
	private final static int BLOCK_2_EAST = 0x2000000;
	private final static int BLOCK_2_SOUTH = 0x8000000;
	private final static int BLOCK_2_WEST = 0x20000000;
	private final static int FULL_BLOCK = BLOCK_0 + BLOCK_1 + BLOCK_2 + BLOCK_3;
	private final static int BLOCK_NORTH = FULL_BLOCK + BLOCK_0_NORTH
			+ BLOCK_1_NORTH + BLOCK_2_NORTH;
	private final static int BLOCK_SOUTH = FULL_BLOCK + BLOCK_0_SOUTH
			+ BLOCK_1_SOUTH + BLOCK_2_SOUTH;
	private final static int BLOCK_WEST = FULL_BLOCK + BLOCK_0_WEST
			+ BLOCK_1_WEST + BLOCK_2_WEST;
	private final static int BLOCK_EAST = FULL_BLOCK + BLOCK_0_EAST
			+ BLOCK_1_EAST + BLOCK_2_EAST;

	public PathFinder(final Tile start, final Tile end) {
		super();
		this.start = start;
		this.end = end;
	}

	public PathFinder(final Tile end) {
		super();
		this.start = Players.getLocal().getLocation();
		this.end = end;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean traverse(final EnumSet<TraversalOption> options) {
		return getNext() != null && tilePath.traverse(options);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isValid() {
		return getNext() != null
				&& !Players.getLocal().getLocation().equals(getEnd());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Tile getNext() {
		tiles = findPath(start, end);
		if (tiles == null) {
			return null;
		}
		tilePath = Walking.newTilePath(tiles);
		return tilePath.getNext();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Tile getStart() {
		return start;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Tile getEnd() {
		tiles = findPath(start, end);
		if (tiles == null) {
			return null;
		}
		tilePath = Walking.newTilePath(tiles);
		return tiles[tiles.length - 1];
	}

	/**
	 * Returns the calculated TilePath that is currently providing data to this
	 * PathFinder.
	 * 
	 * @return The current TilePath; or <code>null</code>.
	 */
	public TilePath getCurrentTilePath() {
		tiles = findPath(start, end);
		if (tiles == null) {
			return null;
		}
		tilePath = Walking.newTilePath(tiles);
		return tilePath;
	}

	protected Tile[] findPath(final Tile end) {
		return findPath(Players.getLocal().getLocation(), end);
	}

	protected Tile[] findPath(Tile start, Tile end) {
		int base_x = Game.getMapBase().getX(), base_y = Game.getMapBase()
				.getY();
		int curr_x = start.getX() - base_x, curr_y = start.getY() - base_y;
		int dest_x = end.getX() - base_x, dest_y = end.getY() - base_y;
		flags = Walking.getCollisionFlags(Game.getFloorLevel());
		if (flags == null || curr_x < 0 || curr_y < 0 || curr_x >= flags.length
				|| curr_y >= flags.length || dest_x < 0 || dest_y < 0
				|| dest_x >= flags.length || dest_y >= flags.length) {
			return null;
		}
		HashSet<Node> open = new HashSet<Node>();
		HashSet<Node> closed = new HashSet<Node>();
		Node curr = new Node(curr_x, curr_y);
		Node dest = new Node(dest_x, dest_y);
		curr.f = heuristic(curr, dest);
		open.add(curr);
		while (!open.isEmpty()) {
			curr = lowest_f(open);
			if (curr.equals(dest)) {
				return path(curr, base_x, base_y);
			}
			open.remove(curr);
			closed.add(curr);
			for (Node next : successors(curr)) {
				if (!closed.contains(next)) {
					double t = curr.g + dist(curr, next);
					boolean use_t = false;
					if (!open.contains(next)) {
						open.add(next);
						use_t = true;
					} else if (t < next.g) {
						use_t = true;
					}
					if (use_t) {
						next.prev = curr;
						next.g = t;
						next.f = t + heuristic(next, dest);
					}
				}
			}
		}
		return null;
	}

	private double heuristic(Node start, Node end) {
		double dx = start.x - end.x;
		double dy = start.y - end.y;
		if (dx < 0)
			dx = -dx;
		if (dy < 0)
			dy = -dy;
		return dx < dy ? dy : dx;
	}

	private double dist(Node start, Node end) {
		if (start.x != end.x && start.y != end.y) {
			return 1.41421356;
		} else {
			return 1.0;
		}
	}

	private Node lowest_f(Set<Node> open) {
		Node best = null;
		for (Node t : open) {
			if (best == null || t.f < best.f) {
				best = t;
			}
		}
		return best;
	}

	private Tile[] path(Node end, int base_x, int base_y) {
		LinkedList<Tile> path = new LinkedList<Tile>();
		Node p = end;
		while (p != null) {
			path.addFirst(p.toTile(base_x, base_y));
			p = p.prev;
		}
		return path.toArray(new Tile[path.size()]);
	}

	private LinkedList<Node> successors(final Node t) {
		final LinkedList<Node> tiles = new LinkedList<Node>();
		final int x = t.x;
		final int y = t.y;
		if ((flags[x][y] & BLOCK_SOUTH) == 0) {
			tiles.add(new Node(x, y - 1));
		}
		if ((flags[x][y] & BLOCK_WEST) == 0) {
			tiles.add(new Node(x - 1, y));
		}
		if ((flags[x][y] & BLOCK_NORTH) == 0) {
			tiles.add(new Node(x, y + 1));
		}
		if ((flags[x][y] & BLOCK_EAST) == 0) {
			tiles.add(new Node(x + 1, y));
		}
		return tiles;
	}
}